package com.gitee.wsl.doc.prosemirror.model.fragment

import com.gitee.wsl.doc.prosemirror.model.Schema
import com.gitee.wsl.doc.prosemirror.model.fragment.ext.insertInto
import com.gitee.wsl.doc.prosemirror.model.fragment.ext.removeRange
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put

// A slice represents a piece cut out of a larger document. It stores not only a fragment, but also
// the depth up to which nodes on both side are ‘open’ (cut through).
@Suppress("EqualsWithHashCodeExist")
data class Slice(
    // Create a slice. When specifying a non-zero open depth, you must make sure that there are
    // nodes of at least that depth at the appropriate side of the fragment—i.e. if the fragment is
    // an empty paragraph node, `openStart` and `openEnd` can't be greater than 1.
    //
    // It is not necessary for the content of open nodes to conform to the schema's content
    // constraints, though it should be a valid start/end/middle for such a node, depending on which
    // sides are open.

    // The slice's content.
    val content: Fragment,
    // The open depth at the start of the fragment.
    val openStart: Int,
    // The open depth at the end.
    val openEnd: Int
) {

    // The size this slice would add when inserted into a document.
    val size: Int
        get() = this.content.size - this.openStart - this.openEnd

    fun insertAt(pos: Int, fragment: Fragment): Slice? {
        val content = insertInto(this.content, pos + this.openStart, fragment) ?: return null
        return Slice(content, this.openStart, this.openEnd)
    }

    fun removeBetween(from: Int, to: Int): Slice {
        return Slice(
            this.content.removeRange( from + this.openStart, to + this.openStart),
            this.openStart,
            this.openEnd
        )
    }

    // Tests whether this slice is equal to another slice.
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Slice) return false

        if (content != other.content) return false
        if (openStart != other.openStart) return false
        if (openEnd != other.openEnd) return false

        return true
    }

    override fun toString(): String {
        return "$content($openStart,$openEnd)"
    }

    // Convert a slice to a JSON-serializable representation.
    fun toJSON(): JsonObject? {
        if (this.content.size == 0) return null
        return buildJsonObject {
            put("content", content.toJSON())
            if (openStart > 0) put("openStart", openStart)
            if (openEnd > 0) put("openEnd", openEnd)
        }
    }

    companion object {
        // The empty slice.
        val empty = Slice(Fragment.Companion.empty, 0, 0)

        // Deserialize a slice from its JSON representation.
        fun fromJSON(schema: Schema, json: JsonObject?): Slice {
            if (json == null) return empty
            val openStart: Int = json["openStart"]?.jsonPrimitive?.int ?: 0
            val openEnd: Int = json["openEnd"]?.jsonPrimitive?.int ?: 0
            return Slice(Fragment.Companion.fromJSON(schema, json["content"]!!.jsonArray), openStart, openEnd)
        }

        // Create a slice from a fragment by taking the maximum possible open value on both side of
        // the fragment.
        fun maxOpen(fragment: Fragment, openIsolating: Boolean = true): Slice {
            var openStart = 0
            var openEnd = 0
            var n = fragment.firstChild
            while (n != null && !n.isLeaf && (openIsolating || n.type.spec.isolating == false)) {
                openStart++
                n = n.firstChild
            }
            n = fragment.lastChild
            while (n != null && !n.isLeaf && (openIsolating || n.type.spec.isolating == false)) {
                openEnd++
                n = n.lastChild
            }
            return Slice(fragment, openStart, openEnd)
        }
    }

    override fun hashCode(): Int {
        var result = openStart
        result = 31 * result + openEnd
        result = 31 * result + content.hashCode()
        result = 31 * result + size
        return result
    }
}